Skip to content

feat: show badge on top liked packages, link to leaderboard#2459

Open
serhalp wants to merge 10 commits intomainfrom
serhalp/feat/likes-leaderboard
Open

feat: show badge on top liked packages, link to leaderboard#2459
serhalp wants to merge 10 commits intomainfrom
serhalp/feat/likes-leaderboard

Conversation

@serhalp
Copy link
Copy Markdown
Member

@serhalp serhalp commented Apr 10, 2026

🔗 Linked issue

N/A 😶

🧭 Context

Social likes are fun. Having users engage with the community, discover packages, and share socially about like ranks is fun.

📚 Description

Show a small rank badge next to the likes counter/button when a package is in the top 10 most-liked packages, and link that badge to a new in-app likes leaderboard page. For now at least, this is the only way to reach the leaderboard page.

npmx.top.liked.demo.v2.mp4

Both are powered by server-side fetching of the likes leaderboard API (https://tangled.org/baileytownsend.dev/npmx-likes-leaderboard), maintained by Bailey (@fatfingers23), who has agreed to treat this as a production service.

API fetches degrade gracefully: on failure, no badge is shown on the package page, and the leaderboard page shows a message indicating that the data is unavailable.

Successful fetches are cached for 1 hour, and are only revalidated in the background, following a stale-while-revalidate-style pattern (this is existing behaviour from server/plugins/fetch-cache).

The leaderboard page is itself cached with ISR, with a revalidation time of 15 minutes.

Here's a fallback screen in case of missing data or failure to load the data:

Screen Shot 2026-04-25 at 18 27 44

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 25, 2026 11:48pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 25, 2026 11:48pm
npmx-lunaria Ignored Ignored Apr 25, 2026 11:48pm

Request Review

@serhalp serhalp changed the title feat: add badge to top-10 most-liked packages, leaderboard page feat: show badge on top liked packages, link to leaderboard Apr 10, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 10, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/fr-FR.json Localization changed, will be marked as complete. 🔄️
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 94.21053% with 11 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/pages/leaderboard/likes.vue 94.11% 9 Missing ⚠️
app/components/Package/Likes.vue 93.75% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a likes leaderboard page displaying the most-liked packages with a podium for the top three entries and a ranked list below
    • Introduced a "top rank" badge on package like components, showing when a package ranks in the leaderboard
    • Extended internationalisation for the leaderboard and package UI across multiple languages
  • Tests

    • Added comprehensive test coverage for leaderboard functionality and accessibility compliance

Walkthrough

A new likes leaderboard feature is introduced with a dedicated page route that fetches and displays ranked packages by total likes. The likes component is enhanced to track a package's top rank position. New server utilities fetch leaderboard data from an external API, enrich entries with npm packument metadata and Microlink homepage previews, and retrieve individual package rank information. Internationalisation strings and cache configurations are added to support the feature.

Changes

Cohort / File(s) Summary
Likes Leaderboard Page
app/pages/leaderboard/likes.vue, nuxt.config.ts
New leaderboard page displaying ranked packages by likes with podium and ranked list layouts. ISR caching configured with 900-second expiration. Supports loading skeleton states and unavailable messaging.
Leaderboard API & Utilities
server/api/leaderboard/likes.get.ts, server/utils/likes-leaderboard.ts
New API endpoint handler that retrieves and enriches likes leaderboard entries. Utility module provides leaderboard normalisation, external API integration, packument lookup, and per-package rank resolution with caching and error handling.
Homepage Metadata
server/utils/npm-homepage.ts
New utility for fetching and caching homepage metadata from Microlink API, supporting image proxying and structured data validation for preview and logo assets.
Likes Component Enhancement
app/components/Package/Likes.vue, server/api/social/likes/[...pkg].get.ts, server/utils/atproto/utils/likes.ts
Extended likes component to display top-rank badge with computed UI labels and optimistic state snapshots. Updated API response to include topLikedRank field. Utility functions return rank information alongside like counts.
Type Definitions
shared/types/social.ts
Added topLikedRank property to PackageLikes type and introduced new LikesLeaderboardEntry type with rank, package metadata, and enriched asset fields.
Internationalisation
i18n/locales/en.json, i18n/locales/fr-FR.json, i18n/schema.json
New i18n keys for likes top-rank UI text, leaderboard labels, and availability messaging. Schema updated to validate new keys including leaderboard.likes section and per-week-short unit.
Configuration & Constants
shared/utils/constants.ts, shared/utils/fetch-cache-config.ts
Added constants for Microlink and leaderboard API URLs. Extended fetch cache allowlist to include external API origins.
Test Fixtures & Mocking
test/fixtures/likes-leaderboard.ts, test/fixtures/microlink/*.json, test/fixtures/mock-routes.cjs
New fixture builder for leaderboard entries and Microlink response JSON files. Mock routes wired to intercept Microlink API requests and return fixture data.
Tests
test/nuxt/a11y.spec.ts, test/nuxt/components/Package/Likes.spec.ts, test/nuxt/pages/LikesLeaderboardPage.spec.ts, test/unit/server/utils/likes-leaderboard.spec.ts, test/unit/server/utils/npm-homepage.spec.ts
Extended accessibility test coverage for likes component and new leaderboard page. Added component spec validating top-rank badge rendering and state persistence. Added leaderboard page spec for both populated and empty states. Added unit tests for leaderboard fetching, enrichment, rank lookup, and homepage metadata resolution.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PageRoute as Leaderboard Page
    participant Handler as Leaderboard Handler
    participant LeaderboardAPI as External Leaderboard API
    participant NpmRegistry as npm Registry
    participant MicrolicAPI as Microlink API
    participant GitHubAPI as GitHub API

    Client->>PageRoute: Load /leaderboard/likes
    PageRoute->>Handler: GET /api/leaderboard/likes
    Handler->>LeaderboardAPI: Fetch likes leaderboard (limit=10)
    LeaderboardAPI-->>Handler: LikesLeaderboardEntry[]
    
    loop For each leaderboard entry
        Handler->>NpmRegistry: Fetch packument (description, homepage)
        NpmRegistry-->>Handler: Package metadata
        
        Handler->>MicrolicAPI: Fetch homepage metadata (preview/logo)
        MicrolicAPI-->>Handler: HomepageMetadata
        
        Handler->>GitHubAPI: Fetch repo stars & weekly downloads
        GitHubAPI-->>Handler: Stars & download count
    end
    
    Handler-->>PageRoute: Enriched LikesLeaderboardEntry[]
    PageRoute-->>Client: Render podium + ranked list
Loading
sequenceDiagram
    participant Component as Likes Component
    participant Handler as Likes Handler
    participant RankAPI as Rank Lookup
    participant LikesUtil as Likes Util

    Component->>Handler: GET /api/social/likes/[...pkg]
    par Concurrent Fetch
        Handler->>LikesUtil: getLikes(event, packageName)
        LikesUtil-->>Handler: {totalLikes, userHasLiked}
    and Concurrent Fetch
        Handler->>RankAPI: getTopLikedRank(event, subjectRef)
        RankAPI-->>Handler: number | null
    end
    
    Handler-->>Component: {totalLikes, userHasLiked, topLikedRank}
    Component->>Component: Compute UI labels (like/unlike, tooltip, badge text)
    Component-->>Component: Render likes counter + top-rank badge (if rank exists)
Loading

Suggested reviewers

  • graphieros
  • ghostdevv
  • alexdln
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: displaying a badge for top-liked packages and linking to a leaderboard page, which aligns with the primary changes across the changeset.
Description check ✅ Passed The description is directly related to the changeset, providing clear context, motivation, and technical details about the likes leaderboard feature implementation, including caching strategy and graceful degradation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch serhalp/feat/likes-leaderboard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

serhalp added 4 commits April 25, 2026 17:40
Show a small rank badge next to the likes counter/button when a package is in the
top 10 most-liked packages, and link that badge to a new in-app likes
leaderboard page. For now at least, this is the only way to reach the leaderboard page.

Both are powered by server-side fetching of the likes leaderboard API
(https://tangled.org/baileytownsend.dev/npmx-likes-leaderboard), maintained by Bailey.

Fetches degrade gracefully: no badge is shown on the package page, and the leaderboard page shows a
message indicating that the data is unavailable.

Successful fetches are cached for 1 hour, and are only revalidated in the background, following a
stale-while-revalidate pattern (this is existing behaviour from `server/plugins/fetch-cache`).

The leaderboard page is itself cached with ISR, with a revalidation time of 15 minutes.
This defers this very optional fetch to later and avoids caching
degraded/failed responses.
}

const repositoryRef = parseRepoUrl(rawRepositoryUrl)
if (!repositoryRef || repositoryRef.provider !== 'github') {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do support a dozen git providers, but... pragmatically, we know the top, like... 1000 most liked packages are on GitHub, so since this is just for the top 10 it didn't seem worth overengineering right now.

Comment on lines +251 to +252
const leaderboard = await getLikesLeaderboard(event)
return leaderboard?.find(entry => entry.subjectRef === subjectRef)?.rank ?? null
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may look stupid from a perf perspective but getLikesLeaderboard is cached for an hour (with ISR semantics) so I think it's quite fine.

@serhalp serhalp force-pushed the serhalp/feat/likes-leaderboard branch from 262c38d to d5859e9 Compare April 25, 2026 23:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (10)
shared/utils/constants.ts (1)

19-21: Consider making the leaderboard API URL configurable via runtime config.

LIKES_LEADERBOARD_API_URL hardcodes a third-party Railway deployment URL. If the API host ever moves (different region, custom domain, staging environment, or a self-hosted alternative), this will require a code change and redeploy. Exposing it via runtimeConfig.public (with this constant as the default) would give operators a runtime escape hatch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/utils/constants.ts` around lines 19 - 21, The constant
LIKES_LEADERBOARD_API_URL hardcodes a deployment URL; change it to be
configurable at runtime by reading a public runtime config value (e.g.
runtimeConfig.public.likesLeaderboardApiUrl) with the existing string as the
default. Replace the exported constant with a getter (or export a function like
getLikesLeaderboardApiUrl) that returns
runtimeConfig.public.likesLeaderboardApiUrl ||
'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes',
and update all call sites that import LIKES_LEADERBOARD_API_URL to call the
getter/function instead so operators can override the URL without redeploying.
Ensure you reference runtime config APIs used by your app (e.g.,
useRuntimeConfig or access process.env if appropriate) when implementing the
getter.
i18n/locales/en.json (1)

801-801: Hardcoded 10 in description couples copy to API limit.

"The 10 most liked packages on npmx right now." hardcodes the leaderboard size. If the ?limit=10 constant is ever changed (or made configurable), this string will go out of sync silently. Consider parameterising with a {count} placeholder driven by the same source as the API request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@i18n/locales/en.json` at line 801, The description string "The 10 most liked
packages on npmx right now." hardcodes the leaderboard size; replace it with a
parameterised placeholder like "{count}" and wire the same count value used by
the API request (the limit query param or limit constant) into the i18n
formatter so the displayed number comes from the same variable that sets
?limit=10; update the locale key (the "description" entry) and ensure the
view/controller that builds the request passes that count into the i18n
translate/format call so the text and the API request remain in sync.
server/api/social/likes/[...pkg].get.ts (1)

27-36: Parallel fetch reads cleanly; relies on getTopLikedRank never throwing.

The current getTopLikedRank implementation is safe (it bottoms out in getLikesLeaderboard's try/catch returning null), so Promise.all won't reject due to leaderboard failures. If a future refactor ever lets getTopLikedRank throw, likes responses would 502 — consider Promise.allSettled or a local try/catch around the rank fetch to harden the contract that the rank is "best-effort, never blocks likes".

♻️ Optional defensive variant
-    const [likes, topLikedRank] = await Promise.all([
-      likesUtil.getLikes(packageName, oAuthSession?.did.toString()),
-      getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName)),
-    ])
-
-    return {
-      ...likes,
-      topLikedRank,
-    }
+    const [likesResult, rankResult] = await Promise.allSettled([
+      likesUtil.getLikes(packageName, oAuthSession?.did.toString()),
+      getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName)),
+    ])
+    if (likesResult.status === 'rejected') throw likesResult.reason
+    return {
+      ...likesResult.value,
+      topLikedRank: rankResult.status === 'fulfilled' ? rankResult.value : null,
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/social/likes/`[...pkg].get.ts around lines 27 - 36, The current
parallel fetch uses Promise.all with likesUtil.getLikes and getTopLikedRank
which assumes getTopLikedRank never throws; make the rank fetch best-effort so
failures don’t cause the whole handler to 502 by wrapping the rank call in a
protective construct (either use Promise.allSettled for the two promises and
extract a successful topLikedRank or null on rejection, or perform a separate
try/catch around await getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName))
and set topLikedRank = null on error); keep likesUtil.getLikes
(PackageLikesUtils) awaited normally and return the combined result with
topLikedRank defaulting to null on failure.
server/api/leaderboard/likes.get.ts (1)

5-10: Consider falling back to unenriched entries on enrichment failure.

If enrichLikesLeaderboardEntries rejects (e.g. one of the per-entry homepage/stars fetches throws and isn't internally caught), the entire endpoint errors and the page loses data we already successfully fetched from the upstream leaderboard. A small wrapper around the enrichment call would let you serve raw entries as a graceful fallback, matching the PR's stated "degrade gracefully" intent.

♻️ Suggested fallback
-  const leaderboardEntries = await getLikesLeaderboard(event)
-  if (!leaderboardEntries) return []
-
-  return await enrichLikesLeaderboardEntries(event, leaderboardEntries)
+  const leaderboardEntries = await getLikesLeaderboard(event)
+  if (!leaderboardEntries) return []
+
+  try {
+    return await enrichLikesLeaderboardEntries(event, leaderboardEntries)
+  } catch (err) {
+    console.error('[leaderboard/likes] Enrichment failed, returning raw entries:', err)
+    return leaderboardEntries
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/leaderboard/likes.get.ts` around lines 5 - 10, The endpoint
currently calls enrichLikesLeaderboardEntries(event, leaderboardEntries) and
will error the whole request if enrichment rejects; wrap that call in a
try/catch so you return the raw leaderboardEntries on enrichment failure.
Specifically, in the eventHandler after getLikesLeaderboard returns entries,
call enrichLikesLeaderboardEntries inside a try block and return its result if
successful, but on catch log the error (or processLogger/error) and return
leaderboardEntries as the graceful fallback; keep the initial
getLikesLeaderboard and the function signatures unchanged.
server/utils/likes-leaderboard.ts (2)

90-156: Reduce duplication and length in getLeaderboardEntryMetadata.

The function is 67 lines (above the ~50-line guideline) primarily because the same return object shape is built three times (lines 124–129, 134–139, 142–147). Collapsing it into a single return with the GitHub ref derived inline keeps all field assignments in one place and shrinks the function meaningfully.

♻️ Suggested shape
-    if (!rawRepositoryUrl) {
-      return {
-        packageDescription,
-        weeklyDownloads,
-        homepageUrl,
-        githubRepositoryRef: null,
-      }
-    }
-
-    const repositoryRef = parseRepoUrl(rawRepositoryUrl)
-    if (!repositoryRef || repositoryRef.provider !== 'github') {
-      return {
-        packageDescription,
-        weeklyDownloads,
-        homepageUrl,
-        githubRepositoryRef: null,
-      }
-    }
-
-    return {
-      packageDescription,
-      weeklyDownloads,
-      homepageUrl,
-      githubRepositoryRef: repositoryRef,
-    }
+    const repositoryRef = rawRepositoryUrl ? parseRepoUrl(rawRepositoryUrl) : null
+    return {
+      packageDescription,
+      weeklyDownloads,
+      homepageUrl,
+      githubRepositoryRef:
+        repositoryRef?.provider === 'github' ? repositoryRef : null,
+    }

As per coding guidelines: "Keep functions focused and manageable (generally under 50 lines)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/likes-leaderboard.ts` around lines 90 - 156, The
getLeaderboardEntryMetadata function duplicates the same return shape three
times; refactor it to build all fields once and return a single object: compute
encodedPackageName, packument, downloadsResult and parsedDownloads as now,
derive rawRepositoryUrl and let repositoryRef = rawRepositoryUrl ?
parseRepoUrl(rawRepositoryUrl) : null, then set githubRepositoryRef =
repositoryRef && repositoryRef.provider === 'github' ? repositoryRef : null and
return { packageDescription, weeklyDownloads, homepageUrl, githubRepositoryRef }
in one place (keeping the existing try/catch and use of cachedFetch,
NpmDownloadCountSchema and encodePackageName).

41-50: Regex matches sub-paths; consider tightening.

^https://npmx\.dev/package/(.+)$ will accept https://npmx.dev/package/foo/extra/segments and capture the entire trailing portion as the package name. This would only matter if upstream ever returns malformed/extra-segmented subjectRef values, but a stricter pattern (e.g. capturing only @scope/name or unscoped names) plus an explicit reject for unexpected trailing characters would fail loudly rather than silently produce a junk packageName. Optional given the controlled upstream contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/likes-leaderboard.ts` around lines 41 - 50, The regex in
extractPackageNameFromSubjectRef is too permissive and will capture extra path
segments; tighten it to only accept valid npm package names (either unscoped
[^/]+ or scoped `@scope/name`) and reject any trailing characters so malformed
subjectRef like /package/foo/extra are not accepted; update the pattern used in
extractPackageNameFromSubjectRef to match either `@scope/name` or name (e.g.
^https://npmx\.dev/package/(@[^/]+\/[^/]+|[^/]+)$) and keep the existing
decodeURIComponent/try-catch logic for returning the package name or null on
non-match.
app/pages/leaderboard/likes.vue (2)

119-119: Minor: prefer numeric v-for over hardcoded arrays.

Vue's v-for="n in 3" iterates n = 1, 2, 3 and avoids allocating a literal array on every render. For the 4–10 range you can use v-for="n in 7" and key on n + 3, or Array.from({ length: 7 }, (_, i) => i + 4).

♻️ Proposed tweak
-          <li v-for="rank in [1, 2, 3]" :key="rank" class="space-y-4">
+          <li v-for="rank in 3" :key="rank" class="space-y-4">
-          <li v-for="rank in [4, 5, 6, 7, 8, 9, 10]" :key="rank">
+          <li v-for="i in 7" :key="i + 3">
+            <!-- use (i + 3) wherever `rank` was referenced -->

Also applies to: 166-166, 214-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/leaderboard/likes.vue` at line 119, Replace the hardcoded array
v-for uses with numeric v-for to avoid allocating arrays on each render: change
the top-three loop to v-for="rank in 3" and keep :key="rank"; for the 4–10 block
replace the array with v-for="n in 7" and compute the displayed rank and key as
n + 3 (or use a computed value) so keys remain unique; make the same replacement
for the other two occurrences mentioned (the blocks at the other v-for
locations) ensuring you update any template expressions that referenced the
original array values to use the new loop variable arithmetic.

281-461: Consider extracting the podium card to remove ~180 lines of duplication.

The mobile (lg:hidden) and desktop (hidden lg:grid) podium <ol> blocks render identical card content for highlightedEntries; only the outer list classes and getPodiumItemClass differ. The same duplication exists in the skeleton section (lines 118–160 vs. 162–211). Extracting the per-entry card into a small <PodiumCard> (or a v-slot-driven sub-component) would cut the template roughly in half and make future changes (e.g. metric tweaks, i18n strings) only need to be applied in one place.

Note: The current setup does also have all top-3 metric/title strings duplicated three times, so any tweak (e.g. changing text-xstext-sm) is currently a four-place edit (mobile data, desktop data, mobile skeleton, desktop skeleton).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/leaderboard/likes.vue` around lines 281 - 461, The template
duplicates the entire per-entry card for mobile and desktop (and their
skeletons); extract the repeated card into a new PodiumCard component (or
functional sub-component) that accepts props like entry (or packageName,
homepagePreviewUrl, homepagePreviewWidth/Height, repositoryStars,
weeklyDownloads, totalLikes, rank) and exposes a slot for skeleton rendering;
keep the outer <li> wrapper (where getPodiumItemClass(entry.rank) is applied) in
the parent lists and replace the repeated BaseCard/inner markup with <PodiumCard
:entry="entry" :rank="entry.rank" :to="packageRoute(entry.packageName)" /> (and
use the same for skeletons), ensuring PodiumCard uses compactNumberFormatter,
formatCompactStat and $t internally so metric/i18n changes are centralized.
server/utils/npm-homepage.ts (1)

95-97: Consider logging unexpected Microlink errors for observability.

The bare catch {} swallows everything — including timeouts and unexpected runtime errors — without any signal. The sibling utility in server/utils/likes-leaderboard.ts (lines 207–213) uses console.error('[likes-leaderboard] Failed to fetch likes leaderboard:', ...) for symmetry; consider matching that here so silent Microlink outages are diagnosable in production logs.

📝 Optional logging
-  } catch {
+  } catch (err) {
+    console.error(
+      '[npm-homepage] Failed to fetch homepage metadata:',
+      err instanceof Error ? err.message : 'Unknown error',
+    )
     return emptyHomepageMetadata(homepageUrl)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/npm-homepage.ts` around lines 95 - 97, The empty catch in
server/utils/npm-homepage.ts swallows Microlink errors; change it to catch the
error (e) and log it before returning emptyHomepageMetadata(homepageUrl), e.g.
use console.error with a clear prefix like '[npm-homepage] Failed to fetch
Microlink:' and include the error object so outages/timeouts are observable;
keep the return of emptyHomepageMetadata(homepageUrl) unchanged.
test/unit/server/utils/likes-leaderboard.spec.ts (1)

280-295: Add coverage for getTopLikedRank null/no-match paths.

The single test only covers a happy-path match. Worth adding cases for:

  • Subject ref not present in the leaderboard → returns null.
  • Upstream cachedFetch rejects (so getLikesLeaderboard returns null) → getTopLikedRank also returns null.

These are cheap to add and would protect the ?? null fallback on line 252 of server/utils/likes-leaderboard.ts from a future refactor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/server/utils/likes-leaderboard.spec.ts` around lines 280 - 295, Add
two tests in test/unit/server/utils/likes-leaderboard.spec.ts for
getTopLikedRank: (1) mock cachedFetch to resolve to a leaderboard that does NOT
include the queried subjectRef (use cachedResult with leaderBoard array missing
the entry) and assert getTopLikedRank(createEvent(cachedFetch), '<subjectRef>')
returns null; (2) mock cachedFetch to reject (vi.fn().mockRejectedValue(new
Error(...))) so getLikesLeaderboard returns null, then assert
getTopLikedRank(createEvent(cachedFetch), '<subjectRef>') returns null;
reference getTopLikedRank, getLikesLeaderboard, createEvent and cachedFetch when
adding these specs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/Package/Likes.vue`:
- Around line 113-118: The merge is incorrectly treating null as missing for
topLikedRank; change the assignment in likesData.value so topLikedRank is set to
result.data.topLikedRank when the server explicitly returned null (i.e.,
preserve null), and only fall back to previousLikesState.topLikedRank when the
property is actually absent — use a presence check on result.data (e.g.,
'topLikedRank' in result.data or hasOwnProperty) rather than nullish coalescing;
update the block that spreads previousLikesState and result.data (referencing
likesData.value, previousLikesState, result.data, and topLikedRank) accordingly.

In `@test/unit/server/utils/likes-leaderboard.spec.ts`:
- Around line 137-165: The test uses the Packument type (referenced in
packuments and the fetchNpmPackageMock implementation) but it isn't imported;
add a type-only import for Packument from '#shared/types' at the top of the file
(so Packument is available for the packuments object and the mocked return
typing) to resolve the missing type reference.

In `@test/unit/server/utils/npm-homepage.spec.ts`:
- Around line 22-30: The test fixture loader function loadMicrolinkFixture uses
__dirname which breaks in ESM; replace its usage by deriving a directory from
import.meta.url (the import.meta.url -> fileURLToPath -> path.dirname pattern
used elsewhere) and use that directory when building fixturePath; update
loadMicrolinkFixture to compute const __dirnameEquivalent =
path.dirname(fileURLToPath(import.meta.url)) and then use that variable instead
of __dirname (apply the same change in the analogous test file
test/unit/server/utils/docs/format.spec.ts).

---

Nitpick comments:
In `@app/pages/leaderboard/likes.vue`:
- Line 119: Replace the hardcoded array v-for uses with numeric v-for to avoid
allocating arrays on each render: change the top-three loop to v-for="rank in 3"
and keep :key="rank"; for the 4–10 block replace the array with v-for="n in 7"
and compute the displayed rank and key as n + 3 (or use a computed value) so
keys remain unique; make the same replacement for the other two occurrences
mentioned (the blocks at the other v-for locations) ensuring you update any
template expressions that referenced the original array values to use the new
loop variable arithmetic.
- Around line 281-461: The template duplicates the entire per-entry card for
mobile and desktop (and their skeletons); extract the repeated card into a new
PodiumCard component (or functional sub-component) that accepts props like entry
(or packageName, homepagePreviewUrl, homepagePreviewWidth/Height,
repositoryStars, weeklyDownloads, totalLikes, rank) and exposes a slot for
skeleton rendering; keep the outer <li> wrapper (where
getPodiumItemClass(entry.rank) is applied) in the parent lists and replace the
repeated BaseCard/inner markup with <PodiumCard :entry="entry"
:rank="entry.rank" :to="packageRoute(entry.packageName)" /> (and use the same
for skeletons), ensuring PodiumCard uses compactNumberFormatter,
formatCompactStat and $t internally so metric/i18n changes are centralized.

In `@i18n/locales/en.json`:
- Line 801: The description string "The 10 most liked packages on npmx right
now." hardcodes the leaderboard size; replace it with a parameterised
placeholder like "{count}" and wire the same count value used by the API request
(the limit query param or limit constant) into the i18n formatter so the
displayed number comes from the same variable that sets ?limit=10; update the
locale key (the "description" entry) and ensure the view/controller that builds
the request passes that count into the i18n translate/format call so the text
and the API request remain in sync.

In `@server/api/leaderboard/likes.get.ts`:
- Around line 5-10: The endpoint currently calls
enrichLikesLeaderboardEntries(event, leaderboardEntries) and will error the
whole request if enrichment rejects; wrap that call in a try/catch so you return
the raw leaderboardEntries on enrichment failure. Specifically, in the
eventHandler after getLikesLeaderboard returns entries, call
enrichLikesLeaderboardEntries inside a try block and return its result if
successful, but on catch log the error (or processLogger/error) and return
leaderboardEntries as the graceful fallback; keep the initial
getLikesLeaderboard and the function signatures unchanged.

In `@server/api/social/likes/`[...pkg].get.ts:
- Around line 27-36: The current parallel fetch uses Promise.all with
likesUtil.getLikes and getTopLikedRank which assumes getTopLikedRank never
throws; make the rank fetch best-effort so failures don’t cause the whole
handler to 502 by wrapping the rank call in a protective construct (either use
Promise.allSettled for the two promises and extract a successful topLikedRank or
null on rejection, or perform a separate try/catch around await
getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName)) and set topLikedRank =
null on error); keep likesUtil.getLikes (PackageLikesUtils) awaited normally and
return the combined result with topLikedRank defaulting to null on failure.

In `@server/utils/likes-leaderboard.ts`:
- Around line 90-156: The getLeaderboardEntryMetadata function duplicates the
same return shape three times; refactor it to build all fields once and return a
single object: compute encodedPackageName, packument, downloadsResult and
parsedDownloads as now, derive rawRepositoryUrl and let repositoryRef =
rawRepositoryUrl ? parseRepoUrl(rawRepositoryUrl) : null, then set
githubRepositoryRef = repositoryRef && repositoryRef.provider === 'github' ?
repositoryRef : null and return { packageDescription, weeklyDownloads,
homepageUrl, githubRepositoryRef } in one place (keeping the existing try/catch
and use of cachedFetch, NpmDownloadCountSchema and encodePackageName).
- Around line 41-50: The regex in extractPackageNameFromSubjectRef is too
permissive and will capture extra path segments; tighten it to only accept valid
npm package names (either unscoped [^/]+ or scoped `@scope/name`) and reject any
trailing characters so malformed subjectRef like /package/foo/extra are not
accepted; update the pattern used in extractPackageNameFromSubjectRef to match
either `@scope/name` or name (e.g.
^https://npmx\.dev/package/(@[^/]+\/[^/]+|[^/]+)$) and keep the existing
decodeURIComponent/try-catch logic for returning the package name or null on
non-match.

In `@server/utils/npm-homepage.ts`:
- Around line 95-97: The empty catch in server/utils/npm-homepage.ts swallows
Microlink errors; change it to catch the error (e) and log it before returning
emptyHomepageMetadata(homepageUrl), e.g. use console.error with a clear prefix
like '[npm-homepage] Failed to fetch Microlink:' and include the error object so
outages/timeouts are observable; keep the return of
emptyHomepageMetadata(homepageUrl) unchanged.

In `@shared/utils/constants.ts`:
- Around line 19-21: The constant LIKES_LEADERBOARD_API_URL hardcodes a
deployment URL; change it to be configurable at runtime by reading a public
runtime config value (e.g. runtimeConfig.public.likesLeaderboardApiUrl) with the
existing string as the default. Replace the exported constant with a getter (or
export a function like getLikesLeaderboardApiUrl) that returns
runtimeConfig.public.likesLeaderboardApiUrl ||
'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes',
and update all call sites that import LIKES_LEADERBOARD_API_URL to call the
getter/function instead so operators can override the URL without redeploying.
Ensure you reference runtime config APIs used by your app (e.g.,
useRuntimeConfig or access process.env if appropriate) when implementing the
getter.

In `@test/unit/server/utils/likes-leaderboard.spec.ts`:
- Around line 280-295: Add two tests in
test/unit/server/utils/likes-leaderboard.spec.ts for getTopLikedRank: (1) mock
cachedFetch to resolve to a leaderboard that does NOT include the queried
subjectRef (use cachedResult with leaderBoard array missing the entry) and
assert getTopLikedRank(createEvent(cachedFetch), '<subjectRef>') returns null;
(2) mock cachedFetch to reject (vi.fn().mockRejectedValue(new Error(...))) so
getLikesLeaderboard returns null, then assert
getTopLikedRank(createEvent(cachedFetch), '<subjectRef>') returns null;
reference getTopLikedRank, getLikesLeaderboard, createEvent and cachedFetch when
adding these specs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dea7b5aa-7f1f-4ee9-a5e5-5305dfbd15dc

📥 Commits

Reviewing files that changed from the base of the PR and between ebcfc01 and 8d6e7b0.

📒 Files selected for processing (25)
  • app/components/Package/Likes.vue
  • app/pages/leaderboard/likes.vue
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json
  • i18n/schema.json
  • nuxt.config.ts
  • server/api/leaderboard/likes.get.ts
  • server/api/social/likes/[...pkg].get.ts
  • server/utils/atproto/utils/likes.ts
  • server/utils/likes-leaderboard.ts
  • server/utils/npm-homepage.ts
  • shared/types/social.ts
  • shared/utils/constants.ts
  • shared/utils/fetch-cache-config.ts
  • test/fixtures/likes-leaderboard.ts
  • test/fixtures/microlink/kit.svelte.dev.json
  • test/fixtures/microlink/nuxt.com.json
  • test/fixtures/microlink/react.dev.json
  • test/fixtures/microlink/vuejs.org.json
  • test/fixtures/mock-routes.cjs
  • test/nuxt/a11y.spec.ts
  • test/nuxt/components/Package/Likes.spec.ts
  • test/nuxt/pages/LikesLeaderboardPage.spec.ts
  • test/unit/server/utils/likes-leaderboard.spec.ts
  • test/unit/server/utils/npm-homepage.spec.ts

Comment thread app/components/Package/Likes.vue
Comment thread test/unit/server/utils/likes-leaderboard.spec.ts
Comment thread test/unit/server/utils/npm-homepage.spec.ts
@serhalp serhalp added the needs review This PR is waiting for a review from a maintainer label Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review This PR is waiting for a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant